Skip to content

gh-146152: Fix memory leak in _json encoder error paths#146164

Open
okiemute04 wants to merge 15 commits intopython:mainfrom
okiemute04:fix-json-encoder-leak
Open

gh-146152: Fix memory leak in _json encoder error paths#146164
okiemute04 wants to merge 15 commits intopython:mainfrom
okiemute04:fix-json-encoder-leak

Conversation

@okiemute04
Copy link
Copy Markdown
Contributor

@okiemute04 okiemute04 commented Mar 19, 2026

Fixes #146152

Add PyDict_DelItem calls to remove objects from the markers dict
on all error paths in encoder_listencode_obj. Previously, objects
were only removed on the success path, causing memory leaks when:

  • default() raises an exception
  • RecursionError occurs
  • Nested encoding fails

Only clean up markers on the RecursionError path (PATH B), where
objects accumulate during stack unwinding. Other error paths are
safe because the markers dict is local and will be freed.

Based on review feedback from @raminfp and @serhiy-storchaka.
@okiemute04 okiemute04 force-pushed the fix-json-encoder-leak branch from 51c39f9 to 0597100 Compare March 19, 2026 14:00
Copy link
Copy Markdown
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure that this works. Please add a test.

  • Define default() which creates a new object, adds a weak reference to it to the list, and returns that object.
  • Call JSON encoding, and after catching an exception, call test.support.gc_collect(), then test that all weak references in the list return None.

@okiemute04
Copy link
Copy Markdown
Contributor Author

@serhiy-storchaka Thanks for the review! I've added a test to test_recursion.py that creates objects tracked with weak references, triggers a RecursionError, and verifies all objects are freed after garbage collection.

Copy link
Copy Markdown
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your update, @okiemute04.

The test does not fail with the current code. I am not sure it tests what it should test.

Modules/_json.c Outdated
Py_DECREF(newobj);
if (rv) {
_PyErr_FormatNote("when serializing %T object", obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please revert this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert cosmetic changes. Nothing was properly reverted.

@bedevere-app
Copy link
Copy Markdown

bedevere-app bot commented Mar 22, 2026

A Python core developer has requested some changes be made to your pull request before we can consider merging it. If you could please address their requests along with any other requests in other reviews from core developers that would be appreciated.

Once you have made the requested changes, please leave a comment on this pull request containing the phrase I have made the requested changes; please review again. I will then notify any core developers who have left a review that you're ready for them to take another look at this pull request.

- Add error checking for PyDict_DelItem return value
- Remove extra blank lines in _json.c
- Fix test import order and assertion message
- Update NEWS entry to clearly describe the RecursionError path
@okiemute04
Copy link
Copy Markdown
Contributor Author

@picnixz has addressed your feedback and added error checking for PyDict_DelItem return value, removed extra blank lines in _json.c, fixed import order and assertion message format in test, updated NEWS entry to clearly describe the RecursionError path...thanks once again for the guide, really appreciate

Copy link
Copy Markdown
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not add or remove blank lines randomly.

Comment on lines +140 to +143
try:
self.dumps(obj, default=default)
except Exception:
pass
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use the infinite recursion pattern + catching the RecursionError explicitly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, that's a good suggestion, it makes more sense, I will do that

Modules/_json.c Outdated
Py_DECREF(newobj);
if (rv) {
_PyErr_FormatNote("when serializing %T object", obj);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert cosmetic changes. Nothing was properly reverted.

Modules/_json.c Outdated
}
return -1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert this blank line.

@okiemute04
Copy link
Copy Markdown
Contributor Author

I have made the requested changes, it's good for another review

@picnixz
Copy link
Copy Markdown
Member

picnixz commented Mar 22, 2026

Do not ask for a review if tests are failing please.

- Use support.infinite_recursion() context manager
- Catch RecursionError explicitly instead of all exceptions
- Fix import order with two blank lines after from imports
- Remove extra blank lines in _json.c
- Use Py_DECREF instead of Py_XDECREF
…/cpython into fix-json-encoder-leak

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
@okiemute04
Copy link
Copy Markdown
Contributor Author

@picnixz all addressed!!! Thanks for the feedback

Copy link
Copy Markdown
Member

@picnixz picnixz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will warn it once. I repeated this many times. But do not change unrelates lines and review what your LLM is giving you. I do not have time to lose with unfiltered LLM output.

Comment on lines +1635 to +1643
if (ident != NULL) {
int del_rv = PyDict_DelItem(s->markers, ident);
Py_DECREF(ident);
if (del_rv < 0) {
Py_DECREF(newobj);
return -1;
}

}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (ident != NULL) {
int del_rv = PyDict_DelItem(s->markers, ident);
Py_DECREF(ident);
if (del_rv < 0) {
Py_DECREF(newobj);
return -1;
}
}
if (ident != NULL) {
(void)PyDict_DelItem(s->markers, ident);
}

Actually since we are anyway falling through the return -1 case we can simplify this as follows. Then keep the XDECREF for indent. However explicitly suppress the error code of DelItem.

Comment on lines -1649 to -1653
if (PyDict_DelItem(s->markers, ident)) {
Py_XDECREF(ident);
int del_rv = PyDict_DelItem(s->markers, ident);
Py_DECREF(ident);
if (del_rv < 0) {
return -1;
}
Py_XDECREF(ident);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part does not need modifications..It was already properly doing its job.

Comment on lines +1654 to +1660
if (ident != NULL) {
int del_rv = PyDict_DelItem(s->markers, ident);
Py_DECREF(ident);
if (del_rv < 0) {
return -1;
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one can benefit from the same optimization as my other comment as we anyway return -1.

@okiemute04
Copy link
Copy Markdown
Contributor Author

@picnixz made changes as requested, _json.c Error paths now use (void)PyDict_DelItem; success path unchanged

return [new_obj]
raise TypeError

depth = min(500, sys.getrecursionlimit() - 10)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should move this code inside infinite_recursion() context, since infinite_recursion() changes the recursion limit.

class JSONTestObject:
pass


Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why again has this been changed?

Comment on lines +1 to +2
import weakref
import sys
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import weakref
import sys
import sys
import weakref

Comment on lines +1 to +5
Fix a memory leak in the :mod:`json` module when a RecursionError occurs
during encoding. Previously, objects created in the ``default()`` function
while recursively encoding JSON could remain alive due to being tracked
indefinitely in the encoder's internal circular-reference dictionary. This
change ensures such objects are properly freed after the exception.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Fix a memory leak in the :mod:`json` module when a RecursionError occurs
during encoding. Previously, objects created in the ``default()`` function
while recursively encoding JSON could remain alive due to being tracked
indefinitely in the encoder's internal circular-reference dictionary. This
change ensures such objects are properly freed after the exception.
:mod:`json`: Fix a memory leak when encoding recursive structures fails.

Copy link
Copy Markdown
Member

@serhiy-storchaka serhiy-storchaka left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test does not fail with the unmodified code. Try:

git diff main... Modules/_json.c | patch -p1 --merge -R

Then build and run the test.

This is a fatal flaw. It means either that the test does not serve its purpose, or that the supposed bug does not exist.

I am not actually sure what problem this PR is supposed to solve. If the leak after catching exception -- it does not exist. If keeping some references while we hold the reference to the exception -- it is disputable if this needs to be fixed, and I suspect that the PR does not fix this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_json: stale markers entries on error paths in encoder_listencode_obj

4 participants